/*
    Copyright 2010 Farent-Solutions Ltd.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

*/
package com.farent.solutions.plugins.aribaweb;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.openide.util.Mutex;
import org.netbeans.api.project.Sources;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.java.project.JavaProjectConstants;
import org.netbeans.api.project.Project;
import org.netbeans.modules.java.api.common.SourceRoots;
import org.netbeans.modules.java.api.common.project.ProjectProperties;
import org.netbeans.spi.project.SourceGroupModifierImplementation;
import org.netbeans.spi.project.support.GenericSources;
import org.netbeans.spi.project.support.ant.SourcesHelper;
import org.netbeans.spi.project.support.ant.AntProjectHelper;
import org.netbeans.spi.project.support.ant.PropertyEvaluator;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.ChangeSupport;
import org.openide.util.NbBundle;

/**
 *
 * @author farouka
 */
public class AWSources  implements Sources, PropertyChangeListener, ChangeListener  {

    private static final String BUILD_DIR_PROP = "${" + "build.dir" + "}";    //NOI18N
    private static final String DIST_DIR_PROP = "${" + "dist.dir" + "}";    //NOI18N

    private final Project project;
    private final AntProjectHelper helper;
    private final PropertyEvaluator evaluator;
    private final SourceRoots sourceRoots;
    private final SourceRoots testRoots;
    private boolean dirty;
    private final Map<String,SourceGroup[]> cachedGroups = new ConcurrentHashMap<String,SourceGroup[]>();
    private long eventId;
    private Sources delegate;
    private final ChangeSupport changeSupport = new ChangeSupport(this);
    private SourceGroupModifierImplementation sgmi;
    private final FireAction fireTask = new FireAction();

    AWSources(Project project, AntProjectHelper helper, PropertyEvaluator evaluator,
                SourceRoots sourceRoots, SourceRoots testRoots) {
        this.project = project;
        this.helper = helper;
        this.evaluator = evaluator;
        this.sourceRoots = sourceRoots;
        this.testRoots = testRoots;
        this.sourceRoots.addPropertyChangeListener(this);
        this.testRoots.addPropertyChangeListener(this);
        this.evaluator.addPropertyChangeListener(this);
        delegate = initSources(); // have to register external build roots eagerly
    }

    /**
     * Returns an array of SourceGroup of given type. It delegates to {@link SourcesHelper}.
     * This method firstly acquire the {@link ProjectManager#mutex} in read mode then it enters
     * into the synchronized block to ensure that just one instance of the {@link SourcesHelper}
     * is created. These instance is cleared also in the synchronized block by the
     * {@link AWSources#fireChange} method.
     */
    public SourceGroup[] getSourceGroups(final String type) {
        final SourceGroup[] _cachedGroups = this.cachedGroups.get(type);
        if (_cachedGroups != null) {
            return _cachedGroups;
        }
        return ProjectManager.mutex().readAccess(new Mutex.Action<SourceGroup[]>() {
            public SourceGroup[] run() {
                Sources _delegate;
                long myEventId;
                synchronized (AWSources.this) {
                    if (dirty) {
                        delegate.removeChangeListener(AWSources.this);
                        delegate = initSources();
                        delegate.addChangeListener(AWSources.this);
                        dirty = false;
                    }
                    _delegate = delegate;
                    myEventId = ++eventId;
                }
                SourceGroup[] groups = _delegate.getSourceGroups(type);
                if (type.equals(Sources.TYPE_GENERIC)) {
                    FileObject libLoc = getSharedLibraryFolderLocation();
                    if (libLoc != null) {
                        SourceGroup[] grps = new SourceGroup[groups.length + 1];
                        System.arraycopy(groups, 0, grps, 0, groups.length);
                        grps[grps.length - 1] = GenericSources.group(null, libLoc,
                                "sharedlibraries", // NOI18N
                                NbBundle.getMessage(AWSources.class, "LibrarySourceGroup_DisplayName"),
                                null, null);
                        groups = grps;
                    }
                }
                synchronized (AWSources.this) {
                    if (myEventId == eventId) {
                        AWSources.this.cachedGroups.put(type, groups);
                    }
                }
                return groups;
            }
        });
    }

    private FileObject getSharedLibraryFolderLocation() {
        String libLoc = helper.getLibrariesLocation();
        if (libLoc != null) {
            String libLocEval = evaluator.evaluate(libLoc);
            File file = null;
            if (libLocEval != null) {
                file = helper.resolveFile(libLocEval);
            }
            FileObject libLocFO = FileUtil.toFileObject(file);
            if (libLocFO != null) {
                //#126366 this can happen when people checkout the project but not the libraries description
                //that is located outside the project
                FileObject libLocParent = libLocFO.getParent();
                return libLocParent;
            }
        }
        return null;
    }

    SourceGroupModifierImplementation getSourceGroupModifierImplementation() {
        return sgmi;
    }

    private Sources initSources() {
        final SourcesHelper sourcesHelper = new SourcesHelper(project, helper, evaluator);   //Safe to pass APH
        register(sourcesHelper, sourceRoots, JavaProjectConstants.SOURCES_HINT_MAIN);
        register(sourcesHelper, testRoots, JavaProjectConstants.SOURCES_HINT_TEST);
        sourcesHelper.addNonSourceRoot(BUILD_DIR_PROP);
        sourcesHelper.addNonSourceRoot(DIST_DIR_PROP);
        sourcesHelper.registerExternalRoots(FileOwnerQuery.EXTERNAL_ALGORITHM_TRANSIENT, false);
        sgmi = sourcesHelper.createSourceGroupModifierImplementation();
        return sourcesHelper.createSources();
    }

    private void register(SourcesHelper sourcesHelper, SourceRoots roots, String hint) {
        String[] propNames = roots.getRootProperties();
        String[] rootNames = roots.getRootNames();
        for (int i = 0; i < propNames.length; i++) {
            String prop = propNames[i];
            String displayName = roots.getRootDisplayName(rootNames[i], prop);
            String loc = "${" + prop + "}"; // NOI18N
            String includes = "${" + ProjectProperties.INCLUDES + "}"; // NOI18N
            String excludes = "${" + ProjectProperties.EXCLUDES + "}"; // NOI18N
            sourcesHelper.sourceRoot(loc).includes(includes).excludes(excludes).hint(hint).displayName(displayName)
                    .add() // principal root
                    .type(JavaProjectConstants.SOURCES_TYPE_JAVA).add();    // typed root
        }
    }

    public void addChangeListener(ChangeListener changeListener) {
        changeSupport.addChangeListener(changeListener);
    }

    public void removeChangeListener(ChangeListener changeListener) {
        changeSupport.removeChangeListener(changeListener);
    }

    private void fireChange() {
        synchronized (this) {
            cachedGroups.clear();   //threading: CHM.clear is not atomic, the getSourceGroup may return staled data which is not a problem in this case.
            dirty = true;
        }
        ProjectManager.mutex().postReadRequest(fireTask.activate());
    }

    public void propertyChange(PropertyChangeEvent evt) {
        String propName = evt.getPropertyName();
        // was listening to PROP_ROOT_PROPERTIES, changed to PROP_ROOTS in #143633 as changes
        // from SourceGroupModifierImplementation need refresh too
        if (SourceRoots.PROP_ROOTS.equals(propName)  ||
            BUILD_DIR_PROP.equals(propName)  ||
            DIST_DIR_PROP.equals(propName)) {
            this.fireChange();
        }
    }

    public void stateChanged (ChangeEvent event) {
        this.fireChange();
    }

    private class FireAction implements Runnable {

        private AtomicBoolean fire = new AtomicBoolean();

        public void run() {
            if (fire.getAndSet(false)) {
                changeSupport.fireChange();
            }
        }

        FireAction activate() {
            this.fire.set(true);
            return this;
        }
    };
}
